﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Xml.XPath;
using EasyTest.Logger;
using HtmlAgilityPack;
using OpenQA.Selenium;

namespace EasyTest {

    /// <summary>
    /// Document container (Page, Frame, Modal, etc) with controls to search for.
    /// </summary>
    public class Focus {

        /// <summary>
        /// Gets and Sets the Name of the Focus.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Gets and Sets the parent container of this Focus.
        /// </summary>
        public Focus ParentFocus { get; set; }

        /// <summary>
        /// Load the Focus in its current container.
        /// </summary>
        public virtual void Go() { }

        /// <summary>
        /// Load the Focus in its current container.
        /// </summary>
        /// <param name="forceLoad">True/False to force the container to load this Focus.</param>
        public virtual void Go(bool forceLoad) { }

    }

    /// <summary>
    /// Pate Document container with controls to search for.
    /// </summary>
    public class Page : Focus {

        /// <summary>
        /// Gets and Sets the PageFindBy object for this Page.
        /// </summary>
        public PageFindBy PageFindBy { get; set; }

        /// <summary>
        /// Create Page object and set the name and pageFindBy properties.
        /// </summary>
        /// <param name="name">Name for this Page.</param>
        /// <param name="pageFindBy">PageFindBy object so search with.</param>
        public Page(string name, PageFindBy pageFindBy) {
            Name = name;
            PageFindBy = pageFindBy;
        }

        /// <summary>
        /// Go to the Page specified.
        /// </summary>
        public override void Go() {
            if(this!=Browser.ActiveFocus)
                Go(false);
        }

        /// <summary>
        /// Go to the Page specified with the option for force it to be loaded.
        /// </summary>
        /// <param name="forceLoad">True/False whether or not to force this page to load in the current Browser instance.</param>
        public override void Go(bool forceLoad) {
            if (Browser.ActiveFocus != this | forceLoad) {
                PageFindBy.Go();
                Browser.ActiveFocus = this;
            }

        }
    }

    /// <summary>
    /// Options to set to search the page.
    /// </summary>
    public class PageFindBy {

        /// <summary>
        /// Available search options for the Page.
        /// </summary>
        public enum Option {
            FindByUrl,
            FindByElement,
            FindByTitle
        };

        /// <summary>
        /// Gets and Sets the Option to search by on this page.
        /// </summary>
        public Option FindByOption { get; set; }

        /// <summary>
        /// Gets and Sets the text Value to search by on this page.
        /// </summary>
        public string FindByValue { get; set; }

        /// <summary>
        /// Number of seconds for the FindBy to wait.
        /// </summary>
        public int SecondsToWait { get; set; }

        /// <summary>
        /// Flag to determine whether or not to open the resulting FindBy in a new window.
        /// </summary>
        public bool OpenNewWindow { get; set; }

        /// <summary>
        /// Return the Page matching the title passed.
        /// </summary>
        /// <param name="title">Text of the Title of the page to search for.</param>
        /// <returns>PageFindBy object found by the Title.</returns>
        public static PageFindBy Title(string title) {
            return Title(title, 0);
        }

        /// <summary>
        /// Return the Page matching the title passed.
        /// </summary>
        /// <param name="title">Text of the Title of the page to search for.</param>
        /// <param name="secondsToWait">Number of seconds to wait to perform the search.</param>
        /// <returns>PageFindBy object found by the Title.</returns>
        public static PageFindBy Title(string title, int secondsToWait) {
            PageFindBy pageFindBy = new PageFindBy();
            pageFindBy.FindByOption = Option.FindByTitle;
            pageFindBy.FindByValue = title;
            pageFindBy.SecondsToWait = secondsToWait;
            pageFindBy.OpenNewWindow = false;
            return pageFindBy;
        }

        /// <summary>
        /// Return the Page matching the string representing the Document Element passed.
        /// </summary>
        /// <param name="elementFindBy">Text of the Document Element of the page to search for.</param>
        /// <returns>PageFindBy object found containing the Document Element.</returns>
        public static PageFindBy Element(string elementFindBy) {
            return Element(elementFindBy, 0);
        }

        /// <summary>
        /// Return the Page matching the string representing the Document Element passed.
        /// </summary>
        /// <param name="elementFindBy">Text of the Document Element of the page to search for.</param>
        /// <param name="secondsToWait">Number of seconds to wait to perform the search.</param>
        /// <returns>PageFindBy object found containing the Document Element.</returns>
        public static PageFindBy Element(string elementFindBy, int secondsToWait) {
            PageFindBy pageFindBy = new PageFindBy();
            pageFindBy.FindByOption = Option.FindByElement;
            pageFindBy.FindByValue = elementFindBy;
            pageFindBy.SecondsToWait = secondsToWait;
            pageFindBy.OpenNewWindow = false;
            return pageFindBy;
        }

        /// <summary>
        /// Return the Page matching the URL passed.
        /// </summary>
        /// <param name="url">Text representing the URL address to search for.</param>
        /// <returns>PageFindBy object found by the URL.</returns>
        public static PageFindBy Url(string url) {
            return Url(url, 0, false);
        }

        /// <summary>
        /// Return the Page matching the URL passed.
        /// </summary>
        /// <param name="url">Text representing the URL address to search for.</param>
        /// <param name="secondsToWait">Number of seconds to wait to perform the search.</param>
        /// <returns>PageFindBy object found by the URL.</returns>
        public static PageFindBy Url(string url, int secondsToWait) {
            return Url(url, secondsToWait, false);
        }

        /// <summary>
        /// Return the Page matching the URL passed.
        /// </summary>
        /// <param name="url">Text representing the URL address to search for.</param>
        /// <param name="secondsToWait">Number of seconds to wait to perform the search.</param>
        /// <param name="newWindow"></param>
        /// <returns>PageFindBy object found by the URL.</returns>
        public static PageFindBy Url(string url, int secondsToWait, bool newWindow) {

            PageFindBy pageFindBy = new PageFindBy();
            pageFindBy.FindByOption = Option.FindByUrl;
            pageFindBy.FindByValue = url;
            pageFindBy.SecondsToWait = secondsToWait;
            pageFindBy.OpenNewWindow = newWindow;
            return pageFindBy;
        }

        /// <summary>
        /// Load the current Page represented by this PageFindBy object.
        /// </summary>
        public void Go() {
            switch (FindByOption) {
                case Option.FindByElement:
                    ActivateWindowByElement();
                    break;
                case Option.FindByTitle:
                    ActivateWindowByTitle();
                    break;
                case Option.FindByUrl:
                    ActivateWindowByUrl();
                    break;
                default:
                    break;
            }
        }

        /// <summary>
        /// Activate the window represented by the Page found by a Document Element.
        /// </summary>
        private void ActivateWindowByElement() {
            if (!Action.Exist(FindByValue)) {
                EasyTestLogger.Log(String.Format("Looking for Page with Element:{0}",this.FindByValue));
                #region Check Window by Element
                for (int i = 0; i <= (SecondsToWait * 10); i++) {
                    //Foreach window in window handles
                    foreach (string windowHandle in Browser.Driver.WindowHandles) {
                        try {
                            //switch to window                            
                            Browser.Driver.SwitchTo().Window(windowHandle);
                            Browser.LogDriverInfo();
                        } 
                        catch (Exception e) { }


                        //Check if element is in window, if it does exist return handle. 
                        if (Action.Exist(FindByValue)) {
                            EasyTestLogger.Log(String.Format("Page with Element:{0} found",this.FindByValue));
                            return;
                        }
                        //EasyTestLogger.Log(String.Format("Page {1} does not have Element:{0} checking next", this.FindByValue,Browser.Driver.Title));

                    }
                    //Wait for 1/4 of second. 
                    System.Threading.Thread.Sleep(100);
                    EasyTestLogger.Log(String.Format("Element:{0} not found on any pages waiting and checking again", this.FindByValue));
                }
                #endregion

                throw new SystemException("Unable to find window with element: "+FindByValue);
            }
        }

        /// <summary>
        /// Activate the window represented by the Page found by a matching URL.
        /// </summary>
        private void ActivateWindowByUrl() {
            try {
                if (!Regex.IsMatch(Browser.Driver.Url,this.FindByValue)) {
                    GoThroughEachWindowAndCheckUrl();
                }
            } catch (Exception e) {
                GoThroughEachWindowAndCheckUrl();
            }
        }

        /// <summary>
        ///  Loop through the loaded windows and activate the window represented by the Page found by the matching URL.
        /// </summary>
        private void GoThroughEachWindowAndCheckUrl()
        {
            #region Check for Url
            for (int i = 0; i <= (SecondsToWait * 4); i++) {
                //Foreach window in window handles
                EasyTestLogger.Log(String.Format("Looking for page with url regex:{0}", this.FindByValue));
                for (int h = (Browser.Driver.WindowHandles.Count-1); h > -1; h--)
                {
                    
                //switch to window
                    try
                    {
                        Browser.Driver.SwitchTo().Window(Browser.Driver.WindowHandles[h]);
                        Browser.LogDriverInfo();
                        //Check if window contains url
                        if (Regex.IsMatch(Browser.Driver.Url, this.FindByValue))
                        {
                            EasyTestLogger.Log(String.Format("Page with title {1} has url regex: {0}", this.FindByValue, Browser.Driver.Title));
                            return;
                        }
                    }
                    catch(Exception e){}
                    
                    //EasyTestLogger.Log(String.Format("Page with title {1} with url{2} does not match url regex: {0}", this.FindByValue, Browser.Driver.Title, Browser.Driver.Url));
                }
                //Wait for 1/4 of second. 
                EasyTestLogger.Log(String.Format("Page with url regex: {0} not found, waiting and will try again", this.FindByValue));
                System.Threading.Thread.Sleep(250);
            }
            #endregion
            throw new SystemException("Unable to find window with URL:"+FindByValue);
        }

        /// <summary>
        /// Activate the window represented by the Page found by the matching Title.
        /// </summary>
        private void ActivateWindowByTitle()
        {
            #region Check for Title
            for (int i = 0; i <= (SecondsToWait * 10); i++) {
                //Foreach window in window handles
                EasyTestLogger.Log(String.Format("Looking for Page with Title: {0}", this.FindByValue));
                foreach (string windowHandle in Browser.Driver.WindowHandles)
                {
                    
                    //switch to window
                    try {
                        
                        Browser.Driver.SwitchTo().Window(windowHandle);
                        Browser.LogDriverInfo();
                        //Check if window contains url
                        if (Browser.Driver.Title.Contains(FindByValue)) {
                            EasyTestLogger.Log(String.Format("Page with Title{1} matches page title: {0}", this.FindByValue,Browser.Driver.Title));
                            return;
                        }
                        EasyTestLogger.Log(String.Format("Page with Title{1} does not match page title: {0}", this.FindByValue, Browser.Driver.Title));
                    } catch (Exception e) {
                        //If window is closed before we can check it, then go to the next window
                    }

                }
                //Wait for 1/4 of second. 
                EasyTestLogger.Log(String.Format("Page with title: {0} not found, waiting and will try again", this.FindByValue));
                System.Threading.Thread.Sleep(100);
            }
            #endregion
            throw new SystemException("Unable to find window with title:"+FindByValue);
        }
    }

    /// <summary>
    /// Frame Document container with controls to search for.
    /// </summary>
    public class Frame : Focus {

        /// <summary>
        /// Gets and Sets the string representing the object to find in this Frame.
        /// </summary>
        public string FrameFindBy { get; set; }

        /// <summary>
        /// Number of seconds for the FindBy to wait.
        /// </summary>
        public int SecondsToWait { get; set; }

        /// <summary>
        /// Gets and Sets the string representing the Document Element to find in this Frame.
        /// </summary>
        public string FrameFindByElement { get; set; }

        /// <summary>
        /// Create a new Frame object.
        /// </summary>
        /// <param name="name">Name of the frame.</param>
        /// <param name="frameFindBy">Text representing the object to find in this frame.</param>
        /// <param name="page">Parent page object that contains this frame.</param>
        public Frame(string name, string frameFindBy, Focus page) {
            Name = name;
            ParentFocus = page;
            FrameFindBy = frameFindBy;
        }

        /// <summary>
        /// Create a new Frame object.
        /// </summary>
        /// <param name="name">Name of the frame.</param>
        /// <param name="frameFindBy">Text representing the object to find in this frame.</param>
        /// <param name="page">Parent page object that contains this frame.</param>
        /// <param name="secondsToWait">Number of seconds to wait to perform the search.</param>
        public Frame(string name, string frameFindBy, Focus page, int secondsToWait)
            : this(name, frameFindBy, page) {
            SecondsToWait = secondsToWait;
        }

        /// <summary>
        /// Load the current Page that this Frame is found on represented by this FrameFindBy object.
        /// </summary>
        public override void Go() {
            Go(false);
        }

        /// <summary>
        /// Load the current Page that this Frame is found on represented by this FrameFindBy object.
        /// </summary>
        /// <param name="isCalledByChild"></param>
        public override void Go(bool isCalledByChild) {
            if (Browser.ActiveFocus != this) {
                if (ParentFocus != null)
                    ParentFocus.Go(true);

                if (ParentFocus == null)
                    Browser.Driver.SwitchTo().DefaultContent();

                Browser.Driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(0));
                for (int i = 0; i <= (SecondsToWait * 10); i++) {
                    try {
                        //string pageSource = GetFrames(Browser.Driver.PageSource);
                        string frameUrl = Browser.Driver.Url;
                        Browser.LogDriverInfo();
                        Browser.Driver.SwitchTo().Frame(Browser.Driver.FindElement(Action.GetFindBy(FrameFindBy)));
                        string newFrameUrl = Browser.Driver.Url;
                        if (frameUrl != newFrameUrl)
                            break;

                    } catch (Exception e) {
                        System.Threading.Thread.Sleep(100);
                    }
                }
                Browser.Driver.Manage().Timeouts().ImplicitlyWait(Browser.MaxWait);
                Browser.ActiveFocus = this;
            }
        }

        /// <summary>
        /// Get a list of Frames located in the Source address passed.
        /// </summary>
        /// <param name="pageSource">Page HTML source to be parsed looking for Frames.</param>
        /// <returns></returns>
        private string GetFrames(string pageSource) {
            string framesList = "";
            HtmlAgilityPack.HtmlDocument htmlDoc = new HtmlAgilityPack.HtmlDocument();
            htmlDoc.OptionFixNestedTags = true;
            htmlDoc.OptionOutputAsXml = true;
            htmlDoc.LoadHtml(pageSource);

            HtmlNodeCollection iframes = htmlDoc.DocumentNode.SelectNodes("//iframe");
            HtmlNodeCollection frames = htmlDoc.DocumentNode.SelectNodes("//frame");
            if (iframes != null) {
                foreach (var frame in iframes) {
                    framesList += frame.OuterHtml;
                }
            }
            if (frames != null) {
                foreach (var frame in frames) {
                    framesList += frame.OuterHtml;
                }
            }
            return framesList;
        }
    }

    /// <summary>
    /// HTML Modal Document container with controls to search for.
    /// </summary>
    public class Modal : Focus {

        /// <summary>
        /// Gets and Sets the Selenium By object to search for an element in this Modal.
        /// </summary>
        public string FindByElement { get; set; }

        /// <summary>
        /// Number of seconds to wait.
        /// </summary>
        public int SecondsToWait { get; set; }

        /// <summary>
        /// Create a new Modal object.
        /// </summary>
        /// <param name="name">Name of the modal.</param>
        /// <param name="page">Parent page object that contains this modal.</param>
        /// <param name="findByElement"> Selenium By object to search for an element.</param>
        public Modal(string name, Focus focus, string findByElement) {
            Name = name;
            ParentFocus = focus;
            FindByElement = findByElement;
        }

        /// <summary>
        /// Create a new Modal object.
        /// </summary>
        /// <param name="name">Name of the modal.</param>
        /// <param name="page">Parent page object that contains this modal.</param>
        /// <param name="findByElement"> Selenium By object to search for an element.</param>
        /// <param name="secondsToWait">Number of seconds to wait to perform the search.</param>
        public Modal(string name, Focus focus, string findByElement, int secondsToWait)
            : this(name, focus, findByElement) {
            SecondsToWait = secondsToWait;
        }

        /// <summary>
        /// Load the current Page that this Modal is found on represented by the FindByElement object.
        /// </summary>
        public override void Go() {
            Go(false);
        }

        /// <summary>
        /// Load the current Page that this Modal is found on represented by this FindByElement object.
        /// </summary>
        /// <param name="forceLoad">True/False whether or not to force the containing page to be loaded in an existing browser object.</param>
        public override void Go(bool forceLoad) {
            if (ParentFocus != null)
                ParentFocus.Go();
            Browser.Driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(0));
            for (int i = 0; i <= (SecondsToWait * 10); i++) {
                try {
                    if (Action.IsDisplayed(FindByElement)) {
                        Browser.Driver.Manage().Timeouts().ImplicitlyWait(Browser.MaxWait);
                        return;

                    }
                } catch (Exception e) {
                    System.Threading.Thread.Sleep(100);
                }
            }
            Browser.Driver.Manage().Timeouts().ImplicitlyWait(Browser.MaxWait);
            throw new System.Exception("Modal with Element: " + FindByElement.ToString() + " on ParentFocus: " + ParentFocus.Name +
                                       " could not be found");
        }
    }
}
